Skip to content

Add Redis-ready caching, sessions, presence, and event-state APIs#53

Merged
fuzziecoder merged 1 commit intomainfrom
codex/implement-redis-caching-features
Feb 25, 2026
Merged

Add Redis-ready caching, sessions, presence, and event-state APIs#53
fuzziecoder merged 1 commit intomainfrom
codex/implement-redis-caching-features

Conversation

@fuzziecoder
Copy link
Owner

@fuzziecoder fuzziecoder commented Feb 25, 2026

Motivation

  • Improve backend performance and UX by adding a cache layer for hot reads and moving transient state out of memory into an optionally Redis-backed store.
  • Harden authentication and brute-force protection by persisting active sessions and rate-limit state in cache so tokens can be revoked and login throttling scales.
  • Provide primitives for real-time features (presence, temporary event state) needed by interactive UIs.

Description

  • Add a new backend/cache.js module that provides optional Redis connectivity with an automatic in-memory fallback plus helpers for JSON caching (getOrSetJsonCache), session storage (sessionStore), rate limiting (rateLimiter), presence (presenceStore), and temporary event-state (eventStateStore).
  • Integrate cache flows into backend/server.js so login uses cache-backed rate limiting and writes an active session, auth-protected routes verify both token and active session, and POST /api/auth/logout invalidates cached sessions.
  • Cache the read-heavy endpoints GET /api/catalog and GET /api/spots, and add presence and event-state endpoints: POST /api/presence/heartbeat, GET /api/presence/active, PUT|POST /api/events/state/:eventKey, and GET /api/events/state/:eventKey.
  • Extend backend/env.js with Redis/cache-related env vars and update backend/README.md to document the new features and environment settings (e.g. REDIS_URL, REDIS_KEY_PREFIX, CACHE_DEFAULT_TTL_SECONDS, PRESENCE_TTL_SECONDS, EVENT_STATE_DEFAULT_TTL_SECONDS).

Testing

  • Ran static syntax checks node --check backend/cache.js && node --check backend/server.js && node --check backend/env.js, which succeeded.
  • Attempted to start the server with PORT=4100 VITE_SUPABASE_URL=https://example.com VITE_SUPABASE_ANON_KEY=abcdefghijk node backend/server.js, but runtime start was blocked by a missing runtime dependency (dotenv) in this environment.
  • Attempted to install dependencies with npm but the install failed due to registry access restrictions, so runtime integration tests requiring external packages could not be completed here.

Codex Task


Open with Devin

Summary by CodeRabbit

  • New Features

    • Added user logout endpoint
    • Added real-time presence tracking endpoints
    • Added event state management endpoints
    • Implemented role-based access control for orders and bills endpoints
    • Enhanced catalog and spot retrieval performance with caching
    • Implemented rate limiting and temporary blocks for login attempts
  • Security

    • Added signed token-based authentication with expiry validation
    • Implemented active session verification for protected routes

@vercel
Copy link

vercel bot commented Feb 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
brocode-spot-update-app Building Building Preview, Comment Feb 25, 2026 2:05pm

@fuzziecoder fuzziecoder self-assigned this Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e5fa696 and 49bdb4b.

📒 Files selected for processing (4)
  • backend/README.md
  • backend/cache.js
  • backend/env.js
  • backend/server.js

📝 Walkthrough

Walkthrough

This PR introduces Redis-backed caching with in-memory fallback, session management via token hashing, cache-backed rate limiting, real-time presence tracking endpoints, and temporary event state storage. Authentication now validates active sessions before token verification and enforces role-based access controls.

Changes

Cohort / File(s) Summary
Documentation
backend/README.md
Updated to reflect new caching system (Issue #31), session/auth improvements (Issue #30), and newly available endpoints including presence, event state, logout, and cached catalog/spots endpoints.
Caching Layer
backend/cache.js
Introduces comprehensive caching module with Redis client fallback to MemoryStore; exports cache utilities, session store (token hashing, active session tracking), rate limiter (failure tracking, blocking), presence store (heartbeats, active user listing), and event state store (TTL-based state persistence).
Environment Configuration
backend/env.js
Extends schema with optional Redis-related variables: REDIS_URL, REDIS_KEY_PREFIX, CACHE_DEFAULT_TTL_SECONDS, PRESENCE_TTL_SECONDS, and EVENT_STATE_DEFAULT_TTL_SECONDS.
Server Integration
backend/server.js
Refactors authentication flow to async validation with active session checks before token verification; replaces in-memory rate limiting with cache-backed rateLimiter; adds caching for GET /api/catalog and GET /api/spots; adds new endpoints for logout, presence heartbeat/listing, and event state management; updates multiple handlers to await async getUserFromAuthHeader.

Sequence Diagrams

sequenceDiagram
    actor Client
    participant Server
    participant SessionStore as Session Store
    participant TokenValidator as Token Validator
    
    Client->>Server: POST /api/orders<br/>(Authorization: Bearer token)
    Server->>SessionStore: hasActiveSession(token)
    alt Session Active
        SessionStore-->>Server: true
        Server->>TokenValidator: validateToken(token)
        alt Token Valid
            TokenValidator-->>Server: {userId, role, expiry}
            Server->>Server: Check role-based permissions
            alt Authorized
                Server-->>Client: 200 OK + response
            else Forbidden
                Server-->>Client: 403 Forbidden
            end
        else Token Invalid
            Server-->>Client: 401 Unauthorized
        end
    else Session Inactive
        SessionStore-->>Server: false
        Server-->>Client: 401 Unauthorized
    end
Loading
sequenceDiagram
    actor Client
    participant Server
    participant PresenceStore as Presence Store
    participant Cache as Cache/Redis
    
    Client->>Server: POST /api/presence/heartbeat<br/>{user, payload}
    Server->>PresenceStore: heartbeat(user, payload)
    PresenceStore->>Cache: set user presence data (TTL)
    Cache-->>PresenceStore: stored
    PresenceStore->>Cache: add user to presence set (TTL)
    Cache-->>PresenceStore: added
    PresenceStore-->>Server: success
    Server-->>Client: 200 OK
    
    Note over Client,Cache: Later query for active presence
    Client->>Server: GET /api/presence/active?spotId=X
    Server->>PresenceStore: listActive(spotId)
    PresenceStore->>Cache: enumerate presence set<br/>(filter by spotId)
    Cache-->>PresenceStore: [active users]
    PresenceStore-->>Server: user list
    Server-->>Client: 200 OK {users}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

apertre3.0, hard

Poem

🐰 Hops through Redis fields so bright,
Sessions cached, presence in flight,
Rate limits bounce, events take their stance,
A caching waltz—let the backend dance!

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/implement-redis-caching-features

Comment @coderabbitai help to get the list of available commands and usage tips.

@fuzziecoder fuzziecoder merged commit 3eea30a into main Feb 25, 2026
4 of 5 checks passed
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CORS Access-Control-Allow-Methods header missing PUT, blocking browser requests to event-state endpoint

The PR adds a PUT /api/events/state/:eventKey endpoint at backend/server.js:391, but the Access-Control-Allow-Methods header in sendJson only lists 'GET,POST,DELETE,OPTIONS'. Since PUT is a non-simple HTTP method, browsers will issue a CORS preflight OPTIONS request, receive the allowed-methods list without PUT, and block the actual PUT request entirely.

Root Cause and Impact

The sendJson helper at backend/server.js:112 sets a static allowed-methods list:

'Access-Control-Allow-Methods': 'GET,POST,DELETE,OPTIONS',

The new event-state write endpoint at backend/server.js:391 accepts PUT:

if ((method === 'PUT' || method === 'POST') && path.startsWith('/api/events/state/')) {

Since PUT triggers a CORS preflight and the preflight response (routed through the OPTIONS handler at backend/server.js:146 which also calls sendJson) does not include PUT in the allowed methods, all browser-based PUT requests to this endpoint will fail. Only POST will work from browsers, making the PUT method dead code for any browser client.

(Refers to line 112)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +175 to +178
const attempts = await cache.client.incr(attemptsKey);
if (attempts === 1) {
await cache.client.set(attemptsKey, String(attempts), { EX: Math.ceil(windowMs / 1000) });
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Race condition in recordFailure: SET after INCR can overwrite concurrent increments, resetting the rate-limit counter

When the first failed login attempt occurs, rateLimiter.recordFailure calls INCR then conditionally SET with a TTL when attempts === 1. Between these two operations (which are separate Redis round-trips), a concurrent request can also INCR the same key, and the subsequent SET from the first request overwrites the counter back to "1".

Detailed Explanation

At backend/cache.js:175-178:

const attempts = await cache.client.incr(attemptsKey);
if (attempts === 1) {
  await cache.client.set(attemptsKey, String(attempts), { EX: Math.ceil(windowMs / 1000) });
}

Race scenario with Redis:

  1. Request A: INCR attemptsKey → returns 1
  2. Request B: INCR attemptsKey → returns 2
  3. Request A: SET attemptsKey "1" EX ttloverwrites value back to 1, losing Request B's increment
  4. Request B: attempts === 2, skips the SET branch

The counter is now "1" instead of "2", effectively discarding one failed attempt. Under sustained attack with concurrent requests, this can repeatedly reset the counter, allowing significantly more login attempts than maxAttempts before a block is triggered.

Fix: Use EXPIRE (or MemoryStore equivalent) to set the TTL without overwriting the value, e.g.:

if (attempts === 1) {
  await cache.client.expire(attemptsKey, Math.ceil(windowMs / 1000));
}
Prompt for agents
In backend/cache.js, rateLimiter.recordFailure (lines 175-178): replace the SET call that overwrites the INCR result with an EXPIRE-style call that only sets the TTL without changing the value. For the MemoryStore class, add an expire(key, seconds) method that sets this.expiries without touching this.values. For Redis, use the native EXPIRE command. This avoids the race where a concurrent INCR between the first request's INCR and SET gets lost when SET resets the value.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +5 to 6
import { cache, eventStateStore, getOrSetJsonCache, presenceStore, rateLimiter, sessionStore } from './cache.js';
import "./env.js";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 cache.js reads env vars before dotenv.config() runs due to import ordering in server.js

The cache.js module is imported before env.js in server.js, so its top-level process.env reads execute before dotenv.config() loads the .env file. This means REDIS_URL, REDIS_KEY_PREFIX, CACHE_DEFAULT_TTL_SECONDS, and PRESENCE_TTL_SECONDS from a .env file will not be picked up by the cache module.

Root Cause and Impact

In backend/server.js:5-6:

import { cache, ... } from './cache.js';
import "./env.js";

cache.js has top-level code at lines 3-6:

const REDIS_URL = process.env.REDIS_URL;
const REDIS_KEY_PREFIX = process.env.REDIS_KEY_PREFIX || 'brocode';
const CACHE_DEFAULT_TTL_SECONDS = Number(process.env.CACHE_DEFAULT_TTL_SECONDS || 60);
const PRESENCE_TTL_SECONDS = Number(process.env.PRESENCE_TTL_SECONDS || 60);

And line 122 runs await createCacheClient() which uses REDIS_URL.

Since ES module imports execute in dependency order and cache.js includes a top-level await, it fully initializes before env.js calls dotenv.config(). If a user configures REDIS_URL=redis://localhost:6379 in their .env file (rather than as an actual environment variable), the cache module will see REDIS_URL as undefined and silently fall back to the in-memory store, ignoring the Redis configuration entirely.

Suggested change
import { cache, eventStateStore, getOrSetJsonCache, presenceStore, rateLimiter, sessionStore } from './cache.js';
import "./env.js";
import "./env.js";
import { cache, eventStateStore, getOrSetJsonCache, presenceStore, rateLimiter, sessionStore } from './cache.js';
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant